<?php
/**
 *DLDIndexer is class to create full-index and manage the full-index
 *DLDIndexer cut chinese words with scws and save the index result in mongodb
 *
 * @author Liang Ye hui
 * @version 1.0.0
 */
class IndexerD extends CApplicationComponent
{
    public $mongodb = "";
    public $scws = "";
	public $mongoOptions = array();
	public $enable = true;

    /*create a instance of this class
     *@param array $mongoOptions the config options of mongodb
	 *@param int $scwsMulti the policy of cutting words of scws.it is 6 by default.if making indexes for human names,8 is recommended
	*/
    public function __construct($mongoOptions =array()){
        /*if(!extension_loaded("scws")){
            //throw new Exception("the SCWS module is not loaded");
			return false;
		}
        if(!extension_loaded("mongo")){
            //throw new Exception("the MongoDB module is not loaded");
			return false;
		}*/
		if($mongoOptions){
            $this->mongoOptions = $mongoOptions;
			$this->init();
		} 
		/*
		$this->mongodb = new DLDMongoD($mongoOptions['host'],$mongoOptions['port'],$mongoOptions['database']);
		$this->scws = scws_new();
        $this->scws->set_charset('utf8');
        $this->scws->set_dict("/usr/local/scws/etc/dict.utf8.xdb");
		$this->scws->set_rule("/usr/local/scws/etc/rules.utf8.ini");
		$this->scws->set_ignore(true); //设定分词返回结果时是否去除一些特殊的标点符号之类
		$this->scws->set_multi($scwsMulti);//设定分割词时候的规则，1~15。1|2|4|8 = short|duality|zmain|zall. 按位异或的 1 | 2 | 4 | 8 分别表示: 短词 | 二元 | 主要单字 | 所有单字
		var_export($this->mongoOptions);*/
    }

	public function init(){
		try{
		    //DLDMongoD::setPoolSize(-1);//设定mongodb的最大连接数，负数表示不限制
		    $this->mongodb = new DLDMongoD($this->mongoOptions['host'],$this->mongoOptions['port'],$this->mongoOptions['database']);
		    $this->scws = scws_new();
            $this->scws->set_charset('utf8');
            $this->scws->set_dict("/usr/local/scws/etc/dict.utf8.xdb");
		    $this->scws->set_rule("/usr/local/scws/etc/rules.utf8.ini");
		    $this->scws->set_ignore(true); //设定分词返回结果时是否去除一些特殊的标点符号之类
		    $this->scws->set_multi(6);//设定分割词时候的规则，1~15。1|2|4|8 = short|duality|zmain|zall. 按位异或的 1 | 2 | 4 | 8 分别表示: 短词 | 二元 | 主要单字 | 所有单字
		}
		catch(Exception $e){
           // echo  $e->getMessage();
		   $this->enable = false;
		   return false;
		}

		return $this;
	}

    public function setScwsMulti($scwsMulti = 6){
		try{
		    if($this->scws){
               $this->scws->set_multi($scwsMulti);
			   return true;
	        }
		}
		catch(Exception $e){
            return false;
		}
		return false;
	}

    /**
	 *create indexes
     *@param string $tableName this is the name of collection in mongodb which stores the index words.it is not necessary that this name is same to mysql table name
	 *@param array $items they are the field names needing index and the values. it is not necessary that this name is same to mysql field name
	 *@param mixed $id it is the id of the record in mysql.it is used whenit's snapshot is maked in mongodb 
	 *@param array $snapshot the elements of it are the fields who is stored in snapshot collection in mongodb.by default,the fields who is made full-index is forced to make snapshot
	 *@return bool true is successful,false is fail
	*/
    public function index($tableName,$items = array(),$id = "",$snapshot = array()){
		if(!$this->enable){
            return false;
		}
		//$offset = 0;
		//$idsItem = array($id=>&$offset);
		if(!is_array($items) || $id === "" ){
            return false;
		}
        
        is_numeric($id) && $id += 0;
        try{
		    foreach($items as $fieldName=>$text){
			    $snapshot[$fieldName] = $text;  //索引字段强制快照
			    $words = $this->gen_words($text);

		        foreach($words as $val){
                    $tmp = $this->mongodb->findOne(array("tag"=>$val["word"],"fieldname"=>$fieldName),$tableName);
			        if($tmp){
                        $tmp["ids"][$id] = $val["off"];
					    unset($tmp['_id']);  //去掉mongodb中的 _id 字段，否则无法更新
				        $this->mongodb->update(array("tag"=>$val['word'],"fieldname"=>$fieldName),array('$set'=>$tmp),$tableName);
			        }
			        else{
                        $indexItem = array(
					              "tag"=>$val["word"],
					              "ids"=>array($id=>$val["off"]),
					              "fieldname"=>$fieldName,
					              "hits"=>0
				                 );
				        $this->mongodb->insert($indexItem,$tableName);
			        }
		        }
		    }
            
			if(strpos($tableName,'_snapshot') === false){
                $tableName .= "_snapshot";
			}
            $snapshot = $this->toInt($snapshot);
		    //快照处理,默认快照记录保存在对应的collection的名字后加"_snapshot"的collection中。如索引保存在名为test的collection中，则它的快照则保存在test_snapshot中
		    $this->mongodb->update(array("id"=>$id),array('$set'=>$snapshot),$tableName,array("upsert"=>true));
		}
		catch(Exception $e){
            return false;
		}
		return true;
	}

    public function toInt($items){
        if(is_array($items)){
            foreach($items as $key=>$val){
                if(is_int($val)){
                    $items[$key] = intval($val); 
				}
                if(is_float($val)){
                    $items[$key] = floatval($val); 
				}
			}
		}
		return $items;
	}


    /**
	*修改全文索引
	*@param string $talbeName mongodb中的collection的名字，就是存在索引词的表的名字，不必与mysql中的表名字相同
	*@param array $items 要索引的字段名字和对应的值，键名为字段名字，值为要进行全文索引的内容 
	*@param int $id 对应被索引的这条记录在mysql中的主键
	*@param array $snapshot 需要保持为快照的字段，每条被索引的数据都会在mongodb中存一个快照，以便加快检索。默认的，被索引的字段强制被快照
    */
    public function modify($tableName,$items = array(),$id = "",$snapshot = array()){
		if(!$this->enable){
            return false;
		}
		if(!is_array($items) || $id === "" ){
            return false;
		}
        is_numeric($id) && $id += 0;
		try{
		    foreach($items as $fieldName=>$text){
			    $snapshot[$fieldName] = $text;  //索引字段强制快照
			    $_nowWords = $this->gen_words($text);
			    $nowWords = array();
			    foreach($_nowWords as $val){
                   $nowWords[]=$val["word"];
			    }
                $tmp = $this->mongodb->findOne(array("id"=>$id),$tableName."_snapshot");
			    $oldText = is_array($tmp)?$tmp[$fieldName]:"";
			    $_oldWords = $oldText!==""?$this->gen_words($oldText):array();
			    $oldWords = array();
			    foreach($_oldWords as $val1){
                    $oldWords[]=$val1["word"];
			    }
			    $_addWords = array_diff($nowWords,$oldWords);
			    $addWords = array();
			    foreach($_nowWords as $val2){
                    if(in_array($val2["word"],$_addWords)){
                        $addWords[] = array("word"=>$val2["word"],"off"=>$val2["off"]);
				    }
			    }
			    $removeWords = array_diff($oldWords,$nowWords);

			    if(!empty($removeWords)){
			        $this->removeIdFromWordRecord($removeWords,$id,$tableName,$fieldName);
			    }
			    if(!empty($addWords)){
			        $this->addIdToWordRecord($addWords,$id,$tableName,$fieldName);
			    }
		    }

			if(strpos($tableName,'_snapshot') === false){
                $tableName .= "_snapshot";
			}
            $snapshot = $this->toInt($snapshot);
		    //快照处理,默认快照记录保存在对应的collection的名字后加"_snapshot"的collection中。如索引保存在名为test的collection中，则它的快照则保存在test_snapshot中
		    $this->mongodb->update(array("id"=>$id),array('$set'=>$snapshot),$tableName,array("upsert"=>true));
		}
		catch(Exception $e){
            return false;
		}
		return true;
	}

    /**
     *从快照表中删除一条快照记录，默认的，也会从索引表的有关索引词中删除此记录的Id
	 *@param string $collection 索引表名字，这是快照表对应的索引表的名字
	 *@param array $ids 要删除的记录的id所组成的数组，Id是这条记录的主键
	 *@param bool $isDelIdFromWord 是否同时从索引表有关的索引词中删除这条记录的id，默认为true，即删除
	 *@return bool 删除成功返回true,否则返回false
	*/
    public function removeSnapshotRecord($collection,$ids,$isDelIdFromWord = true){
		if(!$this->enable){
            return false;
		}
        if(!$collection || !$ids){
            return false;
		}
        //is_numeric($id) && $id += 0;
		strpos($collection,"_snapshot")===false && $collection .= "_snapshot";
		!is_array($ids)  && $ids = array($ids);
		$tmpary = explode("_",$collection);
        $wordsCollection = $tmpary[0];
        try{
		    foreach($ids as $id){
                is_numeric($id) && $id += 0;
			    if($isDelIdFromWord){//从索引表的有关索引词里去除这个id
			        $tmp = $this->mongodb->findOne(array("id"=>$id),$collection);
			        if($tmp){
				        unset($tmp["_id"]);
				        unset($tmp["id"]);
                        foreach($tmp as $field=>$val){
                            $words = "";
					        $words = $this->gen_words($val);
					        foreach($words as $k=>$v){
                                $words[$k] = $v["word"];
					        }
					        if($words){
                                $this->removeIdFromWordRecord($words,$id,$wordsCollection,$field);
					        }
				        }
			        }
		        }
			    $this->mongodb->remove(array("id"=>$id),$collection);
		    }
		}
		catch(Exception $e){
            return false;
		}
		return true;
	}

    /**
	 *create indexes
     *@param string $tableName this is the name of collection in mongodb which stores the index words.it is not necessary that this name is same to mysql table name
	 *@param array $items they are the field names needing index and the values. it is not necessary that this name is same to mysql field name
	 *@param mixed $id it is the id of the record in mysql.it is used whenit's snapshot is maked in mongodb 
	 *@param array $snapshot the elements of it are the fields who is stored in snapshot collection in mongodb.by default,the fields who is made full-index is forced to make snapshot
	 *@return bool true is successful,false is fail
	*/
    public function createSnapshot($tableName,$id = "",$snapshot = array()){
		if(!$this->enable){
            return false;
		}

		if(!is_array($snapshot) && empty($snapshot) || $id === "" ){
            return false;
		}
        
		if(strpos($tableName,'_snapshot') === false){
            $tableName .= "_snapshot";
		}

        is_numeric($id) && $id += 0;
        try{
			$snapshot = $this->toInt($snapshot);
		    //快照处理,默认快照记录保存在对应的collection的名字后加"_snapshot"的collection中。如索引保存在名为test的collection中，则它的快照则保存在test_snapshot中
		    $this->mongodb->update(array("id"=>$id),array('$set'=>$snapshot),$tableName,array("upsert"=>true));
		}
		catch(Exception $e){
            return false;
		}
		return true;
	}

    public function removeWordRecord($collection,$words,$fields){
        if(!$collection || empty($words)){
            return false;
		}

		if(is_string($words)){
            $words = array($words);
		}

		if(is_string($fields)){
            $fields = array($fields);
		}

		foreach($words as $val){
            if(!empty($fields)){
                foreach($fields as $field){
                    $this->mongodb->remove(array("tag"=>$val,"fieldname"=>$field),$collection);
				}
			}
			else{
                $this->mongodb->remove(array("tag"=>$val),$collection);
			}
		}
	}

    public function removeIdFromWordRecord($words,$id,$collection,$field){
        if(!is_array($words) || !$id || !$collection){
            return false;
		}
  
        is_numeric($id) && $id += 0;

        if(is_string($words)){
            $words = array($words);
		}

		foreach($words as $val){
			//从特定field的分词索引记录中删除一个id
            if($field){
                $tmp = $this->mongodb->findOne(array("tag"=>$val,"fieldname"=>$field),$collection);
			    if($tmp){
					$tmpbak = $tmp;
                    unset($tmp["ids"][$id]);
					unset($tmp['_id']);  //去掉mongodb中的 _id 字段，否则无法更新
					if(empty($tmp["ids"])){//如果去掉这个Id后，这个索引词所索引的id已经为0，则直接删除这个索引词记录
                        $this->mongodb->remove($tmpbak,$collection);
					}
					else{
				        $this->mongodb->update(array("tag"=>$val,"fieldname"=>$field),array('$set'=>$tmp),$collection);
					}
			    }
			}
			//删除当前集合collection中所有给定的索引分词中某文章或用户信息记录等的id
			else{
                $tmps = $this->mongodb->find(array("tag"=>$val),$collection);
				if(!empty($tmps)){
				    foreach($tmps as $tmp){
						$tmpbak = $tmp;
                        unset($tmp["ids"][$id]);
					    unset($tmp['_id']);  //去掉mongodb中的 _id 字段，否则无法更新
						if(empty($tmp["ids"])){//如果去掉这个Id后，这个索引词所索引的id数量已经为0，则直接删除这个索引词记录
							$this->mongodb->remove($tmpbak,$collection);
						}
						else{
				            $this->mongodb->update(array("tag"=>$val,"fieldname"=>$tmp["fieldname"]),array('$set'=>$tmp),$collection);
						}
				    }
			    }
			}
		}
	}

    public function addIdToWordRecord($words,$id,$collection,$field){
        if(!is_array($words) || !$id || !$collection){
            return false;
		}

        is_numeric($id) && $id += 0;

		//指定某个字段
		if($field){
		    foreach($words as $val){
                $tmp = $this->mongodb->findOne(array("tag"=>$val["word"],"fieldname"=>$field),$collection);
			    if($tmp){
                    $tmp["ids"][$id] = $val["off"];
					unset($tmp['_id']);  //去掉mongodb中的 _id 字段，否则无法更新
				    $this->mongodb->update(array("tag"=>$val['word'],"fieldname"=>$field),array('$set'=>$tmp),$collection);
			    }
			    else{
                    $indexItem = array(
					              "tag"=>$val["word"],
					              "ids"=>array($id=>$val["off"]),
					              "fieldname"=>$field,
					              "hits"=>0
				                 );
				    $this->mongodb->insert($indexItem,$collection);
			    }
		    }
		}
		else{		
		    foreach($words as $val){
                $tmps = $this->mongodb->find(array("tag"=>$val["word"]),$collection);
				if(empty($tmps)){
                    $indexItem = array(
					             "tag"=>$val["word"],
					             "ids"=>array($id=>$val["off"]),
					             // "fieldname"=>"",
					              "hits"=>0
				                 );
				    $this->mongodb->insert($indexItem,$collection);
				}
				else{
				    foreach($tmps as $tmp){//echo $val["word"];continue;
                        $tmp["ids"][$id] = $val["off"];
					    unset($tmp['_id']);  //去掉mongodb中的 _id 字段，否则无法更新
				        $this->mongodb->update(array("tag"=>$val['word'],"fieldname"=>$tmp['fieldname']),array('$set'=>$tmp),$collection);
				    }
				}
		    }
		}
	}

    public function gen_words($text){
        $words = array();
		try{
            $this->scws->send_text($text);
		    while($set=$this->scws->get_result()){
                foreach($set as $rs){
                    $words[] = $rs;
			    }
		    }
		}
		catch(Exception $e){
            return false;
		}
		return $words;
	}

   /**
   *根据关键字搜索相关的记录
   *@param array $words 要搜索的关键字所组成的数组
   *@param string $collection 要搜索的索引词表的名字
   *@param int $start 在所有的搜索结果中，要返回的记录的开始位置
   *@param int $len 要返回多少条记录，默认为0，表示不限制
   *@param array $searchFields 是个数组，其元素表明要对那些字段用所给的关键字进行搜索
   *@param string $policy 搜索策略，默认为"or",表示对这些关键字进行"或"搜索，只要与其中一个关键字匹配，则列入搜索结果。"and"为"与"搜索，要与所有给定的关键字匹配才列入搜索结果
   *@param bool $onlyId 默认为false，表示返回搜索结果的记录快照。true表示只返回搜索结果的记录的id
   *@param string $searchHitsCollection 搜索统计表，一般为默认就行
   *@return array 如果参数$onlyId为true，则只返回记录的id，否则返回记录的快照
   */
	public function search($words,$collection,$start = 0,$len = 0,$searchFields = array(),$sort='',$fields = array(),$options = array('policy' => 'or','searchHitsCollection' => "search_hits")){
		/*if(!$words || !$collection){
            return false;
		}

        if(!is_array($words)){
            $words = array($words);
		}

        if(!is_array($searchFields)){
            $searchFields = array($searchFields);
		}*/
        
		$isMultiField = false;
        if(count($searchFields) > 0){
			foreach($searchFields as $val){
                $tmpFields[] = array("fieldname"=>$val);
			}
			$searchFields = $tmpFields;
			$isMultiField = true;//判断是否有一个或者一个以上的搜索字段，如果为false表示将搜索全部字段
		}

	    $result = array();
		$flag = false; //判断是否是第一个搜索词搜索，false表示是第一个,and查询的时候会用到
        foreach($words as $val1){
			try{
			    $this->mongodb->update(array('tag'=>$val1,'collection'=>'totalwebsite'),array('$inc'=>array('hits'=>1)),$options['searchHitsCollection'],array('multiple'=>false,'upsert'=>true));//全站搜索次数统计，加1
			    $this->mongodb->update(array('tag'=>$val1,'collection'=>$collection),array('$inc'=>array('hits'=>1)),$options['searchHitsCollection'],array('multiple'=>false,'upsert'=>true));//分表搜索次数统计，加1
		        if($isMultiField){
		            $query['$or'] =$searchFields;
			    }
                $query["tag"] = $val1;
			    $cursor = $this->mongodb->find($query,$collection);
			}
			catch(Exception $e){
                return false;
			}
			//$cursor->snapshot();//从mongodb里取最新的，而不是从缓存中取
			//如果是and查询，只要有一个搜索词搜到的结果为空的话，则说明搜索结果一定为空，直接返回空数组
			if($options['policy'] == 'and' && $cursor->count()<1){
                 return array();
			}
		    foreach($cursor as $rs){
				//print_r($rs);
				if($options['policy'] == 'and' && $flag){//and搜索
                    $result = array_intersect_key($result,$rs["ids"]);
				}
			    else{//or搜索
                    $result = $result+$rs["ids"];
				}

			}
			$flag = true;
		} 

        $ids = empty($result)?null:array_keys($result);
            
		/*if($options['onlyId'] && $ids){
			$ids = $len>0?array_slice($ids,$start,$len):array_slice($ids,$start);
            return $ids;
		}*/
          
		$return = '';		   
		if($ids){	
			$query = array('id'=>array('$in'=>$ids)); 
			//$_result = $this->mongodb->find($query,$collection.'_snapshot');
            $_result = $this->listAll($collection,$query,$start,$len,$sort,$fields);
            foreach($_result as $v){
                unset($v['_id']);
			    //$v['off'] = $result[$v['id']];
				$return[] = $v; 
			}
		}
		return $return;
	}

	public function getHotWords($num = 10,$collection= "totalwebsite"){
		try{
            $cursor = $this->mongodb->find(array('collection'=>$collection),"search_hits",array('tag','hits'))->sort(array('hits'=>-1))->limit($num);
		}
		catch(Exception $e){
            return array();
		}
		$result = array();
		foreach($cursor as $val){
			unset($val['_id']);
            $result[] = $val;
		}
		return $result;
	}

	public function listAll($collection,$query = array(),$start = 0,$len = 0,$sort= '',$fields = array()){
		if(!$this->enable || !$collection)
			return array();
		if(strpos($collection,'_snapshot') === false){
            $collection .= '_snapshot';
		}
		try{
            $cursor = !empty($fields)?$this->mongodb->find($query,$collection,$fields):$this->mongodb->find($query,$collection);
		    $cursor = $start>0?$cursor->skip($start):$cursor;
		    $cursor = $len>0?$cursor->limit($len):$cursor;
		    $cursor = $sort?$cursor->sort($sort):$cursor;
			/*if($sort){
		        if($len){
                    $cursor = $this->mongodb->find($query,$collection,$fields)->sort($sort)->skip($start)->limit($len);
		        }
		        else{
                    $cursor = $this->mongodb->find($query,$collection,$fields)->sort($sort)->skip($start);
		        }
			}
			else{
		        if($len){
                    $cursor = $this->mongodb->find($query,$collection,$fields)->skip($start)->limit($len);
		        }
		        else{
                    $cursor = $this->mongodb->find($query,$collection,$fields)->skip($start);
		        }
			}*/
		}
		catch(Exception $e){
            return array();
		}
		return $cursor;
	}
   
   public function manageHotWords($maxNum = 1000,$HotWordsCollection='search_hits'){
	   try{
           $_collections = $this->mongodb->find(array(),$HotWordsCollection,array('collection')); 
           foreach($_collections as $rs){
		       if(isset($rs['collection'])){
                   $collections[] = $rs['collection'];
		       }
	       }

	       if(is_array($collections)){
               $collections = array_unique($collections);
		       foreach($collections as $collection){
                   $cursor = $this->mongodb->find(array('collection'=>$collection),$HotWordsCollection)->sort(array('hits'=>-1))->skip($maxNum);
			       $delTags = array();
			       foreach($cursor as $r){
                       $delTags[] = $r['tag'];
			       }
			       if($delTags){
                       $this->mongodb->remove(array('collection'=>$collection,'tag'=>array('$in'=>$delTags)),$HotWordsCollection);
			       }
		       }
	       }
	   }
	   catch(Exception $e){
           return false;
	   }
	   return true;
   }

    /*
     *从指定的表中返回符合条件记录数量
	 *@param string $collection 表名
	 *@param array $query 查询条件
	 *@return int 
    */
    public function count($collection,$query = array()){
		if(!$this->enable || !$collection)
			return 0;
		if(strpos($collection,'_snapshot') === false){
            $collection .= '_snapshot';
		}
		try{
            $count = $this->mongodb->find($query,$collection)->count();
		}
		catch(Exception $e){
            return 0;
		}
		return $count;
	}
        
        
        
     /**
     * 从mongo提取的数据列表转换成二维数组
     * @param array $data    从mongo提取的数据集合
     * @return array $result_data
     */
    public function toDataArray($data) {
        $result_data = array();
        if (!empty($data)) {
            foreach ($data as $key => $val) {
                array_shift($val); 
		$tmp = new stdClass();
                $tmp = (object) $val;
                $result_data[] = $tmp;
            }
        }
        return $result_data;
    }
 
    
    /**
     * 从mongo提取的数据列表转换成dataProvider的数据源
     * 用在CActiveDataProvider::setData(dataProvider); 
     * @param array $data    从mongo提取的数据集合
     * @param CActiveRecord  $obj  被转换的模型
     * @return array $dataProvider
     */
    public function toDataProvider($data, $obj) {
        $model = clone $obj;
        $dataProvider = array();
        if (!empty($data)) {
            foreach ($data as $key => $val) {
                array_shift($val);
                foreach ($val as $k => $v) {
                    $model[$k] = $v;
                }
                $dataProvider[] = clone $model;
            }
        }
        return $dataProvider;
    }

	public function __destruct(){
		try{
            if($this->mongodb){
                $this->mongodb->close();
		    }
		    if($this->scws){
                $this->scws->close();
		    }
		}
		catch(Exception $e){
            return false;
		}
	}
}
